/*
 * Copyright (c) 2004-2022, University of Oslo
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors 
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.hisp.dhis.common;

import static org.hisp.dhis.common.DimensionalObjectUtils.asBaseObjects;
import static org.hisp.dhis.common.DisplayProperty.SHORTNAME;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.google.common.base.MoreObjects;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.tuple.Triple;
import org.hisp.dhis.analytics.AggregationType;
import org.hisp.dhis.analytics.QueryKey;
import org.hisp.dhis.eventvisualization.EventRepetition;
import org.hisp.dhis.legend.LegendSet;
import org.hisp.dhis.option.OptionSet;
import org.hisp.dhis.program.Program;
import org.hisp.dhis.program.ProgramStage;

@JacksonXmlRootElement(localName = "dimension", namespace = DxfNamespaces.DXF_2_0)
public class BaseDimensionalObject extends BaseNameableObject implements DimensionalObject {
  @Setter @Getter private UUID groupUUID;

  /** The type of this dimension. */
  private DimensionType dimensionType;

  /**
   * The data dimension type of this dimension. Can be null. Only applicable for {@link
   * DimensionType#CATEGORY}.
   */
  protected DataDimensionType dataDimensionType;

  /** Indicates whether this object should be handled as a data dimension. */
  protected boolean dataDimension = true;

  /**
   * The name of this dimension. For the dynamic dimensions this will be equal to dimension
   * identifier. For the period dimension, this will reflect the period type. For the org unit
   * dimension, this will reflect the level.
   */
  private transient String dimensionName;

  /** The display name to use for this dimension. */
  private transient String dimensionDisplayName;

  /** Holds the value type of the parent dimension. */
  private transient ValueType valueType;

  /** The option set associated with the dimension, if any. */
  private transient OptionSet optionSet;

  /** The dimensional items for this dimension. */
  private List<DimensionalItemObject> items = new ArrayList<>();

  /** Indicates whether all available items in this dimension are included. */
  private boolean allItems;

  /** The legend set for this dimension. */
  protected LegendSet legendSet;

  /** The program stage for this dimension. */
  private ProgramStage programStage;

  /** The program for this dimension. */
  private Program program;

  /** The aggregation type for this dimension. */
  protected AggregationType aggregationType;

  /**
   * Filter. Applicable for events. Contains operator and filter on this format:
   * <operator>:<filter>;<operator>:<filter> Operator and filter pairs can be repeated any number of
   * times.
   */
  private String filter;

  /** Applicable only for events. Holds the indexes relate to the repetition object. */
  private EventRepetition eventRepetition;

  /**
   * A {@link DimensionItemKeywords} defines a pre-defined group of items. For instance, all the OU
   * withing a district
   */
  private DimensionItemKeywords dimensionalKeywords;

  /**
   * Indicates whether this dimension is fixed, meaning that the name of the dimension will be
   * returned as is for all dimension items in the response.
   */
  private boolean fixed;

  // --------------------------------------------------------------------------
  // Constructors
  // --------------------------------------------------------------------------

  // TODO displayName collides with translation solution, rename

  public BaseDimensionalObject() {}

  public BaseDimensionalObject(String dimension) {
    if (dimension != null) {
      with(dimension);
    }
  }

  /**
   * This method will split the given dimension into individual "uid" and assign each one to the
   * respective object, for each internal association.
   *
   * @param qualifiedDimension the dimension. It can be a simple uid like "dimUid", or a qualified
   *     value like "programUid.stageUid.dimUid".
   */
  private void with(String qualifiedDimension) {
    Triple<Program, ProgramStage, DimensionalObject> tripe = asBaseObjects(qualifiedDimension);

    this.program = tripe.getLeft() != null ? tripe.getLeft() : null;
    this.programStage = tripe.getMiddle() != null ? tripe.getMiddle() : null;
    this.uid = tripe.getRight() != null ? tripe.getRight().getUid() : qualifiedDimension;
  }

  public BaseDimensionalObject(
      String dimension, DimensionType dimensionType, List<? extends DimensionalItemObject> items) {
    this(dimension);
    this.dimensionType = dimensionType;
    this.items = new ArrayList<>(items);
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionDisplayName,
      List<? extends DimensionalItemObject> items) {
    this(dimension, dimensionType, items);
    this.dimensionDisplayName = dimensionDisplayName;
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionName,
      String dimensionDisplayName,
      List<? extends DimensionalItemObject> items) {
    this(dimension, dimensionType, items);
    this.dimensionName = dimensionName;
    this.dimensionDisplayName = dimensionDisplayName;
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      List<? extends DimensionalItemObject> items,
      ValueType valueType) {
    this(dimension, dimensionType, null, items);
    this.uid = dimension;
    this.dimensionType = dimensionType;
    this.items = new ArrayList<>(items);
    this.valueType = valueType;
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionName,
      String dimensionDisplayName,
      List<? extends DimensionalItemObject> items,
      DimensionItemKeywords dimensionalKeywords) {
    this(dimension, dimensionType, dimensionName, dimensionDisplayName, items);
    this.dimensionalKeywords = dimensionalKeywords;
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionName,
      String dimensionDisplayName,
      List<? extends DimensionalItemObject> items,
      boolean allItems) {
    this(dimension, dimensionType, dimensionName, dimensionDisplayName, items);
    this.allItems = allItems;
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionName,
      String dimensionDisplayName,
      LegendSet legendSet,
      ProgramStage programStage,
      String filter) {
    this(
        dimension,
        dimensionType,
        dimensionName,
        dimensionDisplayName,
        legendSet,
        programStage,
        filter,
        null,
        null);
  }

  public BaseDimensionalObject(
      String dimension,
      DimensionType dimensionType,
      String dimensionName,
      String dimensionDisplayName,
      LegendSet legendSet,
      ProgramStage programStage,
      String filter,
      ValueType valueType,
      OptionSet optionSet) {
    this(dimension);
    this.dimensionType = dimensionType;
    this.dimensionName = dimensionName;
    this.dimensionDisplayName = dimensionDisplayName;
    this.legendSet = legendSet;
    this.programStage = programStage;
    this.filter = filter;
    this.valueType = valueType;
    this.optionSet = optionSet;

    setProgram();
  }

  // TODO aggregationType in constructors

  // -------------------------------------------------------------------------
  // Logic
  // -------------------------------------------------------------------------

  private void setProgram() {
    if (programStage != null) {
      program = programStage.getProgram();
    }
  }

  @Override
  public String getDimensionName() {
    return dimensionName != null ? dimensionName : uid;
  }

  @Override
  public String getDisplayProperty(DisplayProperty displayProperty) {
    if (SHORTNAME.equals(displayProperty) && getDisplayShortName() != null) {
      return getDisplayShortName();
    } else {
      String name = getDimensionDisplayName();
      return name == null || name.isBlank() ? getDisplayName() : name;
    }
  }

  @Override
  public AnalyticsType getAnalyticsType() {
    return DimensionType.PROGRAM_ATTRIBUTE.equals(dimensionType)
            || DimensionType.PROGRAM_DATA_ELEMENT.equals(dimensionType)
        ? AnalyticsType.EVENT
        : AnalyticsType.AGGREGATE;
  }

  /**
   * Returns the items in the filter as a list. Order of items are preserved. Requires that the
   * filter has the IN operator and that at least one item is specified in the filter, returns null
   * if not.
   */
  @Override
  public List<String> getFilterItemsAsList() {
    final String inOp = QueryOperator.IN.getValue().toLowerCase();
    final int opLen = inOp.length() + 1;

    if (filter == null || !filter.toLowerCase().startsWith(inOp) || filter.length() < opLen) {
      return null;
    }

    String filterItems = filter.substring(opLen, filter.length());

    return new ArrayList<>(Arrays.asList(filterItems.split(DimensionalObject.OPTION_SEP)));
  }

  @Override
  public String getKey() {
    QueryKey key = new QueryKey();

    key.add("dimension", getDimension());
    getItems().forEach(e -> key.add("item", e.getDimensionItem()));

    return key.add("allItems", allItems)
        .addIgnoreNull("legendSet", legendSet)
        .addIgnoreNull("aggregationType", aggregationType)
        .addIgnoreNull("filter", filter)
        .asPlainKey();
  }

  // --------------------------------------------------------------------------
  // Getters and setters
  // --------------------------------------------------------------------------

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public String getDimension() {
    return uid;
  }

  public void setDimension(String dimension) {
    this.uid = dimension;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public DimensionType getDimensionType() {
    return dimensionType;
  }

  public void setDimensionType(DimensionType dimensionType) {
    this.dimensionType = dimensionType;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public DataDimensionType getDataDimensionType() {
    return dataDimensionType;
  }

  public void setDataDimensionType(DataDimensionType dataDimensionType) {
    this.dataDimensionType = dataDimensionType;
  }

  @Override
  public void setDimensionName(String dimensionName) {
    this.dimensionName = dimensionName;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public boolean isDataDimension() {
    return dataDimension;
  }

  public void setDataDimension(boolean dataDimension) {
    this.dataDimension = dataDimension;
  }

  @Override
  public String getDimensionDisplayName() {
    return dimensionDisplayName;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public ValueType getValueType() {
    return valueType;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public OptionSet getOptionSet() {
    return optionSet;
  }

  @Override
  @JsonProperty
  @JsonDeserialize(contentAs = BaseDimensionalItemObject.class)
  @JacksonXmlElementWrapper(localName = "items", namespace = DxfNamespaces.DXF_2_0)
  @JacksonXmlProperty(localName = "item", namespace = DxfNamespaces.DXF_2_0)
  public List<DimensionalItemObject> getItems() {
    return items;
  }

  @Override
  public void setItems(List<DimensionalItemObject> items) {
    this.items = items;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public boolean isAllItems() {
    return allItems;
  }

  public void setAllItems(boolean allItems) {
    this.allItems = allItems;
  }

  @Override
  @JsonProperty
  @JsonSerialize(as = BaseIdentifiableObject.class)
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public LegendSet getLegendSet() {
    return legendSet;
  }

  public void setLegendSet(LegendSet legendSet) {
    this.legendSet = legendSet;
  }

  @Override
  @JsonProperty
  @JsonSerialize(as = BaseIdentifiableObject.class)
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public ProgramStage getProgramStage() {
    return programStage;
  }

  public void setProgramStage(ProgramStage programStage) {
    this.programStage = programStage;
  }

  @Override
  @JsonProperty
  @JsonSerialize(as = BaseIdentifiableObject.class)
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public Program getProgram() {
    return program;
  }

  public void setProgram(Program program) {
    this.program = program;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public AggregationType getAggregationType() {
    return aggregationType;
  }

  public void setAggregationType(AggregationType aggregationType) {
    this.aggregationType = aggregationType;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public String getFilter() {
    return filter;
  }

  public void setFilter(String filter) {
    this.filter = filter;
  }

  @Override
  @JsonProperty("repetition")
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public EventRepetition getEventRepetition() {
    return eventRepetition;
  }

  @Override
  public void setEventRepetition(EventRepetition eventRepetition) {
    this.eventRepetition = eventRepetition;
  }

  @Override
  @JsonIgnore
  public boolean isFixed() {
    return fixed;
  }

  @Override
  public void setFixed(boolean fixed) {
    this.fixed = fixed;
  }

  @Override
  @JsonProperty
  @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
  public DimensionItemKeywords getDimensionItemKeywords() {
    return this.dimensionalKeywords;
  }

  public void setDimensionalKeywords(DimensionItemKeywords dimensionalKeywords) {
    this.dimensionalKeywords = dimensionalKeywords;
  }

  @Override
  public String toString() {
    List<String> itemStr =
        items.stream()
            .map(
                item ->
                    MoreObjects.toStringHelper(DimensionalItemObject.class)
                        .add("uid", item.getUid())
                        .add("name", item.getName())
                        .toString())
            .collect(Collectors.toList());

    return MoreObjects.toStringHelper(this)
        .add("dimension", uid)
        .add("type", dimensionType)
        .add("dimension display name", dimensionDisplayName)
        .add("dimension value type", valueType)
        .add("items", itemStr)
        .toString();
  }
}
